<?php

namespace App\Lib;

use Illuminate\Support\Facades\Redis;

class AppleJwtVerify
{
    private static $supported_algs = array(
        'HS256' => array('hash_hmac', 'SHA256'),
        'HS512' => array('hash_hmac', 'SHA512'),
        'HS384' => array('hash_hmac', 'SHA384'),
        'RS256' => array('openssl', 'SHA256'),
        'RS384' => array('openssl', 'SHA384'),
        'RS512' => array('openssl', 'SHA512'),
    );

    private static $appleAuthKeysUrl = 'https://appleid.apple.com/auth/keys';
    private static $redisKeyAppleAuthKeys = 'CACHE:APPLE_AUTH_KEYS';


    /*private static $appleKeys = '{
      "keys": [
        {
          "kty": "RSA",
          "kid": "86D88Kf",
          "use": "sig",
          "alg": "RS256",
          "n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ",
          "e": "AQAB"
        },
        {
          "kty": "RSA",
          "kid": "eXaunmL",
          "use": "sig",
          "alg": "RS256",
          "n": "4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw",
          "e": "AQAB"
        }
      ]
    }';*/

    // appleUserId
    // 000327.cd00e3974ea8402dbe3a33e6867f1ee6.1006

    // identityToken
    // eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnl3c3kuaW9zLmRlbW8iLCJleHAiOjE1ODY5NDY5NzAsImlhdCI6MTU4Njk0NjM3MCwic3ViIjoiMDAwMzI3LmNkMDBlMzk3NGVhODQwMmRiZTNhMzNlNjg2N2YxZWU2LjEwMDYiLCJjX2hhc2giOiJsQTFkcDlZMnZBVzlFQXlkSWw2MVh3IiwiZW1haWwiOiI5ZXpyMmszaDZzQHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTg2OTQ2MzcwLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.GqZFKMQ3KTG42x2-W7r69nnYqqoBHszI4LBI7m7ysyqBRyt1XSGDPy440F153C8x05VgZgkYi0mIZheCIenIMl5R0unOKrXhvHihgwIKtuvPClRQmAyZYxOWct8xGoPvrRpZr4AkJwxauUxaY8NIoV8-UrNduQcjW8-63-wF9B0F-2p61WZuOEmCoULj2aW7fBoRgFylGbQpXAU_8t32fj1JG3OJzErDJsi1P1CJyKaamd-UpVmgwyaCl0nXMnX0CB0ERqb76M67BHY0ji3VBuIp3uZczEEJMzFtgAevOfgoNYRFicVBr25XoyaWYPxZgYnI-AeUQgvnwHaacx4bkg

    // header
    // {
    //   "kid": "AIDOPK1",
    //   "alg": "RS256"
    // }

    // payload
    // {
    //  "iss": "https://appleid.apple.com",
    //  "aud": "com.fun.AppleLogin",
    //  "exp": 1568721769,
    //  "iat": 1568721169,
    //  "sub": "000580.087c554dce35466fa85c5a5d594d528a.0801",
    //  "c_hash": "z_JcDlpW3B2p7q1GCgkRZQ",
    //  "auth_time": 1568721169
    // }

    // sign
    // WmSa4LzOzYsdwTqAJ_8mub4Ls3eyFkxZoGLoy-U7DatsTd_JEwAs3_OtV4ucmj6ENT3153iCpYY6vBxSQromOMcXsN74IrUQew24y_zflN2g4yU8ZVvBCbTrR_6p9f2fbeWjZiyNcbPCha0dv45E3vBjyHhmffWnk3vyndBBiwwuqod4pyCZ3UECf6Vu-o7dygKFpMHPS1ma60fEswY5d-_TJAFk1HaiOfFo0XbL6kwqAGvx8HnraIxyd0n8SbBVxV_KDxf15hdotUizJDW7N2XMdOGQpNFJim9SrEeBhn9741LWqkWCgkobcvYBZsrvnUW6jZ87SLi15rvIpq8_fw

    /**
     * 运行
     *
     * @param string $appleUserId 苹果用户唯一标识符，它在同一个开发者账号下的所有 App 下是一样的，我们可以用它来与后台的账号体系绑定起来（类似于微信的OpenID）。
     * @param string $identityToken 解析后的identityToken由三部分组成：Header.Payload.Signature
     *
     * @return mixed bool/array
     * */
    public function run($appleUserId, $identityToken)
    {
        $appleJwtVerify = self::appleJwtVerify($appleUserId, $identityToken);
        if (!empty($appleJwtVerify)) {
            return false;
        }

        return [
            'unionid' => '',
            'openid' => $appleUserId,
            'nickname' => '',
            'gender' => 3,
            'province' => '',
            'city' => '',
            'portrait' => '',
        ];
    }

    /**
     * 从苹果服务器读取公钥
     *
     * @param string $kid
     * @param bool $cache
     * @return array
     * */
    private static function appleAuthKeys($kid, $cache = true)
    {
        $data = [];

        do {
            if ($cache) {
                if (Redis::exists(self::$redisKeyAppleAuthKeys)) {
                    $appleKeys = json_decode(Redis::get(self::$redisKeyAppleAuthKeys), true);
                    $appleKeys = array_column($appleKeys, null, 'kid');
                    if (!array_key_exists($kid, $appleKeys)) {
                        $data = self::appleAuthKeys($kid, false);
                        break;
                    }
                    $data = $appleKeys[$kid];
                } else {
                    $data = self::appleAuthKeys($kid, false);
                }
                break;
            }

            $appleKeys = curlRequest(self::$appleAuthKeysUrl);
            $appleKeys = json_decode($appleKeys, TRUE);
            if (empty($appleKeys) or !isset($appleKeys['keys']) or empty($appleKeys['keys'])) { // $msg = '请求苹果服务器失败';
                break;
            }

            // 缓存1H
            Redis::setex(self::$redisKeyAppleAuthKeys, 3600, json_encode($appleKeys['keys'], JSON_UNESCAPED_UNICODE));

            $appleKeys = array_column($appleKeys['keys'], null, 'kid');
            if (!array_key_exists($kid, $appleKeys)) { // $msg = '苹果公钥不匹配';
                break;
            }
            $data = $appleKeys[$kid];

        } while (false);

        return $data;
    }

    /**
     * AppleID登录 JWT校验identityToken
     *
     * @param string $appleUserId 苹果用户唯一标识符，它在同一个开发者账号下的所有 App 下是一样的，我们可以用它来与后台的账号体系绑定起来（类似于微信的OpenID）。
     * @param string $identityToken 解析后的identityToken由三部分组成：Header.Payload.Signature
     * @param string $authorizationCode (使用JWT算法做验证，不需要authorizationCode)
     * @return string
     */
    private static function appleJwtVerify($appleUserId, $identityToken, $authorizationCode = '')
    {
        $msg = '';

        do {
            $token = explode('.', $identityToken);
            if (count($token) != 3) {
                $msg = 'token不合法';
                break;
            }

            $jwtHeader = json_decode(base64_decode($token[0]), true);
            $jwtPayload = json_decode(base64_decode($token[1]), true);
            $jwtSign = $token[2];

            if ($appleUserId !== $jwtPayload['sub']) {
                $msg = '苹果用户ID与token不对应';
                break;
            }

            if ($jwtPayload['exp'] < time()) {
                $msg = 'token已过期';
                break;
            }

            $theAppleKey = self::appleAuthKeys($jwtHeader['kid']);
            if (empty($theAppleKey)) {
                $msg = '苹果公钥不匹配';
                break;
            }

            $pem = self::createPemFromModulusAndExponent($theAppleKey['n'], $theAppleKey['e']);
            $pKey = openssl_pkey_get_public($pem);
            if ($pKey === FALSE) {
                $msg = '生成苹果pem失败';
                break;
            }
            $publicKeyDetails = openssl_pkey_get_details($pKey);

            $ok = self::verify("$token[0].$token[1]", static::urlsafeB64Decode($jwtSign), $publicKeyDetails['key'], $jwtHeader['alg']);
            if (!$ok) {
                $msg = '苹果登录签名校验失败';
                break;
            }

        } while (false);

        return $msg;
    }

    /**
     *
     * Create a public key represented in PEM format from RSA modulus and exponent information
     *
     * @param string $n the RSA modulus encoded in Base64
     * @param string $e the RSA exponent encoded in Base64
     * @return string the RSA public key represented in PEM format
     */
    private static function createPemFromModulusAndExponent($n, $e)
    {
        $modulus = static::urlsafeB64Decode($n);
        $publicExponent = static::urlsafeB64Decode($e);

        $components = array(
            'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus),
            'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent)
        );

        $RSAPublicKey = pack(
            'Ca*a*a*',
            48,
            self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])),
            $components['modulus'],
            $components['publicExponent']
        );

        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
        $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
        $RSAPublicKey = chr(0) . $RSAPublicKey;
        $RSAPublicKey = chr(3) . self::encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey;

        $RSAPublicKey = pack(
            'Ca*a*',
            48,
            self::encodeLength(strlen($rsaOID . $RSAPublicKey)),
            $rsaOID . $RSAPublicKey
        );

        $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
            chunk_split(base64_encode($RSAPublicKey), 64) .
            '-----END PUBLIC KEY-----';

        return $RSAPublicKey;
    }

    /**
     * Decode a string with URL-safe Base64.
     *
     * @param string $input A Base64 encoded string
     *
     * @return string A decoded string
     */
    private static function urlsafeB64Decode($input)
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $padlen = 4 - $remainder;
            $input .= str_repeat('=', $padlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }

    /**
     * DER-encode the length
     *
     * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
     * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
     *
     * @access private
     * @param int $length
     * @return string
     */
    private static function encodeLength($length)
    {
        if ($length <= 0x7F) {
            return chr($length);
        }

        $temp = ltrim(pack('N', $length), chr(0));
        return pack('Ca*', 0x80 | strlen($temp), $temp);
    }

    /**
     * Get the number of bytes in cryptographic strings.
     *
     * @param string
     *
     * @return int
     */
    private static function safeStrlen($str)
    {
        if (function_exists('mb_strlen')) {
            return mb_strlen($str, '8bit');
        }
        return strlen($str);
    }

    /**
     * Verify a signature with the message, key and method. Not all methods
     * are symmetric, so we must have a separate verify and sign method.
     *
     * @param string $msg The original message (header and body)
     * @param string $signature The original signature
     * @param string|resource $key For HS*, a string key works. for RS*, must be a resource of an openssl public key
     * @param string $alg The algorithm
     *
     * @return bool
     *
     * @throws \Exception Invalid Algorithm or OpenSSL failure
     */
    private static function verify($msg, $signature, $key, $alg)
    {
        if (empty(static::$supported_algs[$alg])) {
            throw new \Exception('Algorithm not supported');
        }

        list($function, $algorithm) = static::$supported_algs[$alg];
        switch ($function) {
            case 'openssl':
                $success = openssl_verify($msg, $signature, $key, $algorithm);
                if ($success === 1) {
                    return true;
                } elseif ($success === 0) {
                    return false;
                }
                // returns 1 on success, 0 on failure, -1 on error.
                throw new \Exception(
                    'OpenSSL error: ' . openssl_error_string()
                );
            case 'hash_hmac':
            default:
                $hash = hash_hmac($algorithm, $msg, $key, true);
                if (function_exists('hash_equals')) {
                    return hash_equals($signature, $hash);
                }
                $len = min(static::safeStrlen($signature), static::safeStrlen($hash));

                $status = 0;
                for ($i = 0; $i < $len; $i++) {
                    $status |= (ord($signature[$i]) ^ ord($hash[$i]));
                }
                $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash));

                return ($status === 0);
        }
    }
}